FeedReplyList.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. 'use client';
  2. import { useCallback, useState } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { FeedReply, FeedRepliesResponse } from '@/types/feed/post';
  5. import FeedReplyItem from './FeedReplyItem';
  6. type Props = {
  7. postID: number;
  8. initialReplies: FeedReply[];
  9. initialTotal: number;
  10. onReply: (target: FeedReply) => void;
  11. refreshKey: number;
  12. };
  13. const PER_PAGE = 30;
  14. export default function FeedReplyList({ postID, initialReplies, initialTotal, onReply, refreshKey }: Props) {
  15. const [replies, setReplies] = useState<FeedReply[]>(initialReplies);
  16. const [total, setTotal] = useState<number>(initialTotal);
  17. const [page, setPage] = useState<number>(2);
  18. const [loading, setLoading] = useState<boolean>(false);
  19. const [lastRefreshKey, setLastRefreshKey] = useState<number>(refreshKey);
  20. const loadMore = useCallback(async () => {
  21. if (loading) {
  22. return;
  23. }
  24. setLoading(true);
  25. try {
  26. const res = await fetchApi<FeedRepliesResponse>(`/api/feed/post/${postID}/replies?page=${page}&perPage=${PER_PAGE}`, {
  27. method: 'GET',
  28. silent: true
  29. });
  30. const list = res.data?.list ?? [];
  31. setReplies((prev) => {
  32. const seen = new Set(prev.map((r) => r.id));
  33. const dedup = list.filter((r) => !seen.has(r.id));
  34. return [...prev, ...dedup];
  35. });
  36. setTotal(res.data?.total ?? total);
  37. setPage((p) => p + 1);
  38. } catch (err) {
  39. console.error(err);
  40. } finally {
  41. setLoading(false);
  42. }
  43. }, [loading, postID, page, total]);
  44. const reload = useCallback(async () => {
  45. setLoading(true);
  46. try {
  47. const res = await fetchApi<FeedRepliesResponse>(`/api/feed/post/${postID}/replies?page=1&perPage=${PER_PAGE}`, {
  48. method: 'GET',
  49. silent: true
  50. });
  51. setReplies(res.data?.list ?? []);
  52. setTotal(res.data?.total ?? 0);
  53. setPage(2);
  54. } catch (err) {
  55. console.error(err);
  56. } finally {
  57. setLoading(false);
  58. }
  59. }, [postID]);
  60. if (refreshKey !== lastRefreshKey) {
  61. setLastRefreshKey(refreshKey);
  62. reload();
  63. }
  64. const handleDelete = (id: number) => {
  65. setReplies((prev) => prev.map(r => r.id === id ? { ...r, isDeleted: true, content: '' } : r));
  66. };
  67. if (replies.length === 0) {
  68. return <p className="feed__reply-empty">아직 답글이 없습니다. 첫 답글을 남겨보세요.</p>;
  69. }
  70. const hasMore = replies.length < total;
  71. return (
  72. <div className="feed__reply-list" role="feed">
  73. <div className="feed__reply-count">답글 {total.toLocaleString()}개</div>
  74. {replies.map((reply) => (
  75. <FeedReplyItem
  76. key={reply.id}
  77. reply={reply}
  78. onReply={onReply}
  79. onDelete={handleDelete}
  80. />
  81. ))}
  82. {hasMore && (
  83. <button type="button" className="feed__reply-more-btn" onClick={loadMore} disabled={loading}>
  84. {loading ? '불러오는 중...' : '답글 더 보기'}
  85. </button>
  86. )}
  87. </div>
  88. );
  89. }